动画和过渡

Scripting 通过 Observable / useObservableAnimationTransitionwithAnimation 以及视图的 animation / transition 属性,基本对齐了 SwiftUI 的动画能力,包括:

  • 属性动画:数值、颜色、布局等属性随状态变化平滑过渡
  • 过渡动画:视图插入 / 移除时的进出效果(如淡入淡出、滑入滑出、翻转)
  • 显式动画:通过 withAnimation 包裹一段「状态更新代码」统一加动画

Animation 类

Animation 用来描述「属性变化的时间曲线与节奏」,类似 SwiftUI 的 Animation

工厂方法(创建动画)

Animation.default()

1static default(): Animation
  • 创建一个默认动画(通常是系统预设的 ease-in-out 曲线)
  • 无需配置,适合「只想要一个普通的过渡效果」的场景

示例:

1<Text animation={{
2  animation: Animation.default(),
3  value: value
4}}>默认动画</Text>

Animation.linear(duration?)

1static linear(duration?: DurationInSeconds | null): Animation
  • 匀速动画,整段时间内速度保持恒定
  • duration:动画持续时间(秒),可选,不传时使用默认时长

适合:进度条数值增长、颜色线性变化等。


Animation.easeIn(duration?)

1static easeIn(duration?: DurationInSeconds | null): Animation
  • 开始慢、后面加速
  • 适合:元素「加速进入」的感觉

Animation.easeOut(duration?)

1static easeOut(duration?: DurationInSeconds | null): Animation
  • 开始快、结尾慢
  • 适合:元素「减速停止」的感觉,如卡片滑入后停在目标位置

Animation.bouncy(options?)

1static bouncy(options?: {
2  duration?: DurationInSeconds
3  extraBounce?: number
4}): Animation
  • 带回弹效果的动画

  • 参数:

    • duration:总时长(秒)
    • extraBounce:额外弹性,越大越明显

适合:按钮点击放大回弹、卡片弹出等「有趣」的动效。


Animation.smooth(options?)

1static smooth(options?: {
2  duration?: DurationInSeconds
3  extraBounce?: number
4}): Animation
  • 相对柔和、过渡自然的动画
  • bouncy 相比,弹性感更弱,更偏「丝滑」

Animation.snappy(options?)

1static snappy(options?: {
2  duration?: DurationInSeconds
3  extraBounce?: number
4}): Animation
  • 动作「干脆利落」,响应速度快
  • 常见于触控反馈、选中高亮等瞬间反馈场景

Animation.spring(options?)

1static spring(options?: {
2  blendDuration?: number
3} & ({
4  duration?: DurationInSeconds
5  bounce?: number
6  response?: never
7  dampingFraction?: never
8} | {
9  response?: number
10  dampingFraction?: number
11  duration?: never
12  bounce?: never
13})): Animation

支持两种配置方式(注意互斥):

  1. 基于时长的弹簧动画

    • duration: 动画持续时间
    • bounce: 弹性大小
  2. 物理参数模式

    • response: 响应速度(值越小反馈越快)
    • dampingFraction: 阻尼系数(0~1,越大越「稳」,越小越「弹」)

额外参数:

  • blendDuration:动画混合时长,用于多动画衔接场景(可选)

示例:

1// 简单弹簧
2const anim1 = Animation.spring({
3  duration: 0.4,
4  bounce: 0.3
5})
6
7// 高级弹簧
8const anim2 = Animation.spring({
9  response: 0.25,
10  dampingFraction: 0.7
11})

Animation.interactiveSpring(options?)

1static interactiveSpring(options?: {
2  response?: number
3  dampingFraction?: number
4  blendDuration?: number
5}): Animation
  • 面向「交互驱动」的弹簧动画,例如拖拽结束后的回弹
  • 参数与 spring 的物理参数模式类似,语义更偏向手势交互

0 Animation.interpolatingSpring(options?)

1static interpolatingSpring(options?: {
2  mass?: number
3  stiffness: number
4  damping: number
5  initialVelocity?: number
6} | {
7  duration?: DurationInSeconds
8  bounce?: number
9  initialVelocity?: number
10  mass?: never
11  stiffness?: never
12  damping?: never
13}): Animation

两种配置方式(互斥):

  1. 物理参数模式

    • mass: 质量
    • stiffness: 刚度
    • damping: 阻尼
    • initialVelocity: 初速度(可选)
  2. 时长 + 弹性模式

    • duration: 动画时长
    • bounce: 弹性
    • initialVelocity: 初速度(可选)

适合对动态效果「非常在意手感」的高级场景。


修改已有动画(链式 API)

delay(time)

1delay(time: DurationInSeconds): Animation
  • 使动画延迟 time 秒后再开始
  • 返回一个新的 Animation 实例(原动画不变)

示例:

1const [animValue, setAnimValue] = useState(0)
2const anim = Animation
3  .spring({ duration: 0.4, bounce: 0.3 })
4  .delay(0.2)
5
6<Text animation={{
7  animation: anim,
8  value: animValue
9}>延迟弹簧</Text>

repeatCount(count, autoreverses?)

1repeatCount(count: number, autoreverses?: boolean): Animation
  • 重复执行动画 count
  • autoreverses(默认 true):是否来回反向播放

示例:

1const pulse = Animation
2  .easeIn(0.6)
3  .repeatCount(3, true)
4
5<Text animation={{
6  animation: pulse,
7  value: value
8}}>闪烁三次</Text>

repeatForever(autoreverses?)

1repeatForever(autoreverses?: boolean): Animation
  • 无限次重复动画
  • 适合加载动画、呼吸灯效果等

Animation 实战示例

示例 1:基本大小动画

1import { VStack, Button, Rectangle, useObservable, Animation, withAnimation } from "scripting"
2
3export function Demo() {
4  const size = useObservable(80)
5
6  return <VStack spacing={16}>
7    <Rectangle
8      frame={{ width: size.value, height: size.value }}
9      backgroundColor="blue"
10      animation={{
11        animation: Animation.spring({ duration: 0.3, bounce: 0.2 }),
12        value: size.value
13      }}
14    />
15
16    <Button
17      title="Toggle Size"
18      action={() => {
19        withAnimation(() => {
20          size.setValue(size.value === 80 ? 140 : 80)
21        })
22      }}
23    />
24  </VStack>
25}

Transition 类(视图过渡)

Transition 描述的是视图插入与移除时的「进场 / 退场效果」,对应 SwiftUI 的 AnyTransition

注意:只有当视图在 JSX 中「存在与否」发生变化(如 {visible.value && <Text ... />})时,transition 才会生效。

实例方法

animation(animation?)

1animation(animation?: Animation): Transition
  • 为当前过渡指定(或覆盖)使用的 Animation
  • 不传时使用默认动画

示例:

1const t = Transition
2  .move("bottom")
3  .animation(Animation.spring({ duration: 0.4 }))

combined(other)

1combined(other: Transition): Transition
  • 组合两个过渡效果,类似 SwiftUI 的 .combined
  • 如:向下滑入 + 淡入

示例:

1const t = Transition
2  .move("bottom")
3  .combined(Transition.opacity())

在视图中使用:

1<Text transition={t}>组合过渡</Text>

静态方法(构造不同类型的过渡)

Transition.identity()

1static identity(): Transition
  • 「没有任何过渡」,视图插入 / 移除时不会做动画
  • 通常用于禁用某些分支的过渡效果

Transition.move(edge)

1static move(edge: Edge): Transition
  • 从某个边缘移入 / 移出
  • edge 通常是 "leading" | "trailing" | "top" | "bottom" 等(和 SwiftUI 对齐)

示例:

1<Text transition={Transition.move("leading")}>
2  从左侧滑入 / 滑出
3</Text>

Transition.offset(position?)

1static offset(position?: Point): Transition
  • 通过偏移实现过渡
  • position: { x: number, y: number },默认 { x: 0, y: 0 }

例如:

1<Text
2  transition={Transition.offset({ x: 0, y: 40 })}
3>
4  从下方位移进出
5</Text>

Transition.pushFrom(edge)

1static pushFrom(edge: Edge): Transition
  • 类似导航 push 的效果,从某个边缘推入并把旧内容推走
  • 适合做「页面切换」类效果

Transition.opacity()

1static opacity(): Transition
  • 单纯的淡入 / 淡出
  • Animation 搭配可以控制淡入淡出的节奏

Transition.scale(scale?, anchor?)

1static scale(
2  scale?: number,
3  anchor?: Point | KeywordPoint
4): Transition
  • 缩放过渡

  • scale:缩放比(默认 1)

  • anchor:缩放基准点,支持:

    • Point:如 { x: 0.5, y: 0.5 }
    • KeywordPoint:如 "center""top", "bottom" 等(具体值与 Scripting 内部对齐)

示例:

1<Text
2  transition={Transition.scale(0.8, "center")}
3>
4  缩放进出
5</Text>

Transition.slide()

1static slide(): Transition
  • 类似 SwiftUI 的 .slide,通常是从一侧滑入 / 滑出(具体方向由系统决定)
  • 常用于列表项、简单出现 / 消失效果

Transition.fade(duration?)

1static fade(duration?: DurationInSeconds): Transition
  • 带时长配置的淡入 / 淡出
  • Transition.opacity() 类似,但可以直接指定过渡时间

Flip 系列(翻转过渡)

1static flipFromLeft(duration?: DurationInSeconds): Transition
2static flipFromBottom(duration?: DurationInSeconds): Transition
3static flipFromRight(duration?: DurationInSeconds): Transition
4static flipFromTop(duration?: DurationInSeconds): Transition
  • 类似卡片翻转的 3D 过渡

示例:

1<Text
2  transition={Transition.flipFromLeft(0.4)}
3>
4  左侧翻入 / 翻出
5</Text>

0 Transition.asymmetric(insertion, removal)

1static asymmetric(
2  insertion: Transition,
3  removal: Transition
4): Transition
  • 插入和移除使用不同的过渡效果
  • 典型用法:进入时从下方滑入,离开时淡出

示例:

1const appear = Transition
2  .move("bottom")
3  .combined(Transition.opacity())
4
5const disappear = Transition.opacity()
6
7const t = Transition.asymmetric(appear, disappear)
8
9<Text transition={t}>不对称过渡</Text>

Transition 实战示例

示例:多种过渡效果对比

1const visible = useObservable(true)
2
3return <VStack spacing={12}>
4  {visible.value &&
5    <Text
6      transition={Transition.slide().combined(Transition.opacity())}
7    >
8      Slide + Fade
9    </Text>
10  }
11
12  {visible.value &&
13    <Text
14      transition={Transition.move("leading")}
15    >
16      Move leading
17    </Text>
18  }
19
20  {visible.value &&
21    <Text
22      transition={Transition.scale()}
23    >
24      Scale
25    </Text>
26  }
27
28  <Button
29    title="Toggle"
30    action={() => {
31      withAnimation(() => {
32        visible.setValue(!visible.value)
33      })
34    }}
35  />
36</VStack>

withAnimation:显式动画入口

withAnimation 用来「显式」地将一段状态更新包裹在动画上下文中,类似 SwiftUI 的 withAnimation。 它返回 Promise<void>,方便在异步逻辑中等待动画完成。

重载签名

1function withAnimation(body: () => void): Promise<void>
2function withAnimation(animation: Animation, body: () => void): Promise<void>
3function withAnimation(
4  animation: Animation,
5  completionCriteria: "logicallyComplete" | "removed",
6  body: () => void
7): Promise<void>
  • 第一个重载:使用默认动画

  • 第二个重载:指定动画曲线 / 弹性等

  • 第三个重载:额外指定完成条件

    • "logicallyComplete":动画在时间轴上播放完成时视为完成(典型属性动画)
    • "removed":通常用于涉及过渡的场景,等待相关视图被移出 / 动画结束后再继续逻辑(具体行为依赖底层 SwiftUI)

实际等待的精确时机由内部动画系统决定,一般可理解为「该动画相关的视图不再处于动画中」。


基本用法

默认动画

1const size = useObservable(100)
2
3<Button
4  title="Toggle"
5  action={() => {
6    withAnimation(() => {
7      size.setValue(size.value === 100 ? 200 : 100)
8    })
9  }}
10/>

指定动画

1const visible = useObservable(true)
2
3<Button
4  title="Toggle Panel"
5  action={() => {
6    withAnimation(
7      Animation.spring({ duration: 0.3, bounce: 0.2 }),
8      () => {
9        visible.setValue(!visible.value)
10      }
11    )
12  }}
13/>

在异步函数中等待动画结束

1async function hideThenRunTask() {
2  await withAnimation(Animation.easeOut(0.25), () => {
3    visible.setValue(false)
4  })
5
6  // 此处可以认为相关动画已经结束,再继续耗时任务或导航
7  await doSomethingHeavy()
8}

视图上的 animation / transition 属性

在 Scripting 的视图组件上,可以通过 props 的形式配置动画相关行为:

  • animation?: Animation(属性动画)
  • transition?: Transition(插入 / 移除过渡)

属性动画(animation)

属性动画的核心逻辑:

  • 当某个视图依赖的 Observablevalue 发生变化时
  • 如果该视图设置了 animation={...} 或更新发生在 withAnimation
  • 则 SwiftUI 会对这些属性差异进行插值,从原值平滑过渡到新值

示例:

1const size = useObservable(80)
2
3<Rectangle
4  frame={{
5    width: size.value,
6    height:size.value
7  }}
8  backgroundColor="green"
9  animation={{
10    animation: Animation.spring({ duration: 0.3, bounce: 0.25 }),
11    value: size.value
12  }}
13/>

配合 withAnimation

1<Button
2  title="Grow"
3  action={() => {
4    withAnimation(() => {
5      size.setValue(size.value + 20)
6    })
7  }}
8/>

过渡动画(transition)

过渡动画只在「视图从无到有 / 从有到无」时生效。

关键点:

  • 通常通过条件渲染控制:

    1{visible.value && <Text transition={...}>Hello</Text>}
  • 状态变化本身需要动画上下文(withAnimation 或默认动画)

  • Transition.animation(...) 可为过渡指定特定 Animation

示例:条件面板的进出过渡

1const visible = useObservable(false)
2
3<VStack>
4  {visible.value &&
5    <Text
6      transition={Transition
7        .move("bottom")
8        .combined(Transition.opacity())
9        .animation(Animation.spring({ duration: 0.35, bounce: 0.3 }))
10      }
11    >
12      Panel
13    </Text>
14  }
15
16  <Button
17    title="Toggle Panel"
18    action={() => {
19      withAnimation(() => {
20        visible.setValue(!visible.value)
21      })
22    }}
23  />
24</VStack>

综合示例:列表增删带过渡与属性动画

1import {
2  VStack,
3  HStack,
4  Text,
5  Button,
6  useObservable,
7  Animation,
8  Transition
9} from "scripting"
10
11type Item = { id: string; title: string }
12
13export function AnimatedList() {
14  const items = useObservable<Item[]>([
15    { id: "1", title: "First" },
16    { id: "2", title: "Second" }
17  ])
18
19  function addItem() {
20    withAnimation(Animation.spring({ duration: 0.3 }), () => {
21      const next = items.value.length + 1
22      items.setValue([
23        ...items.value,
24        { id: String(next), title: `Item ${next}` }
25      ])
26    })
27  }
28
29  function removeLast() {
30    if (items.value.length === 0) return
31    withAnimation(Animation.easeOut(0.25), () => {
32      items.setValue(items.value.slice(0, -1))
33    })
34  }
35
36  return <VStack spacing={12}>
37    {items.value.map(item =>
38      <HStack
39        key={item.id}
40        transition={Transition
41          .move("trailing")
42          .combined(Transition.opacity())
43        }
44      >
45        <Text>{item.title}</Text>
46      </HStack>
47    )}
48
49    <HStack spacing={12}>
50      <Button title="Add" action={addItem} />
51      <Button title="Remove Last" action={removeLast} />
52    </HStack>
53  </VStack>
54}

这个示例中:

  • 使用 Observable<Item[]> 作为列表数据源
  • transition 负责列表项插入 / 删除时的滑动 + 淡入淡出
  • withAnimation 包裹增删操作,确保这些更新被动画化